#define OUTPUT_COMMANDLINE
using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Threading;
using System.DirectoryServices.ActiveDirectory;
using System.Runtime.InteropServices;
using ActiveDirectoryUtilities.CommonLibrary;
using ActiveDirectoryUtilities.CommonLibrary.Unsafe;


namespace ActiveDirectoryUtilities.ReplDiag
{
	partial class Program
	{
		/// <summary>
		/// Track how many NCs have been progressed through regardless of success/failure.
		/// </summary>
		private static long _numComplete = 0;

		private static void cleanNCs(ProcessReplicationInformation pri)
		{
			Dictionary<string, DomainControllerReplicationInfo> writableReferencesDC = new Dictionary<string, DomainControllerReplicationInfo>();
			Dictionary<string, int> numberOfCosByNC = new Dictionary<string, int>();
			Dictionary<string, List<DomainControllerReplicationInfo>> writableHostsInNC = new Dictionary<string, List<DomainControllerReplicationInfo>>();

			#region Check to see if the topology is stable.
            if (!((_bypassReplicationStabilityCheck) || (_outputRemoveLingeringObjectsCommandLine)) && ((!pri.IsReplicationToplogyStable) && (!pri.IsReplicationTopologyUnstableDueToLingeringObjects)))
			{
				Console.WriteLine("Error:  Can not build clean topology due to unstable replication state.");
				//since there was an error, exit.
				return;
			}
			else if (pri.IsReplicationTopologyUnstableDueToLingeringObjects)
			{
				Console.WriteLine("Warning:  Replication topology is unstable due to the presence of lingering");
				Console.WriteLine("\tobjects. Multiple cleans will be required to ensure full topology\n\tdiscovery.");
				Console.WriteLine();
			}
			#endregion

			#region Build lists necessary for targeting.
			//Build a list of the servers with writable partitions that have the most inbound
			//	Connections.  This assumes that these are relatively centrally located bridgeheads.
			foreach (DomainControllerReplicationInfo dcri in _dcs)
			{
				foreach (NamingContextInfo nci in dcri.HostedNcs)
				{
					if ((nci.IsWritable) && (!nci.NCName.Contains("schema")))
					{
						if (!numberOfCosByNC.ContainsKey(nci.NCName))
						{
							numberOfCosByNC.Add(nci.NCName, -1);
						}

						if (dcri[nci.NCName].ReplicationConnections.Count > numberOfCosByNC[nci.NCName])
						{
							numberOfCosByNC[nci.NCName] = dcri[nci.NCName].ReplicationConnections.Count;
							if (!writableReferencesDC.ContainsKey(nci.NCName))
							{
								writableReferencesDC.Add(nci.NCName, dcri);
							}
							else
							{
								writableReferencesDC[nci.NCName] = dcri;
							}
						}
					}
				}
			}

			//This code will override the automatically selected referenceDCs based on command line parameters included.
			if (_referenceDCOverride.Count > 0)
			{
				Console.WriteLine();
				foreach (KeyValuePair<string, string> kvp in _referenceDCOverride)
				{
					bool foundDC = false, foundNC = false;
					Console.Write("Attempting to override NC {0} with DC {1}...", kvp.Key, kvp.Value);
					foreach (DomainControllerReplicationInfo dcri in _dcs)
					{
						if (dcri.DCName.CompareTo(kvp.Value) == 0)
						{
							foundDC = true;
							foreach (NamingContextInfo nci in dcri.HostedNcs)
							{
								if (nci.NCName.CompareTo(kvp.Key) == 0)
								{
									Console.WriteLine("\tOverriden");
									writableReferencesDC[kvp.Key] = dcri;
									foundNC = true;
								}
							}
						}
					}
					if (!foundDC)
					{
						Console.WriteLine("\tCould not find DC in forest.");
					}
					else if (!foundNC)
					{
						Console.WriteLine("\tCould not find NC on specified DC.");
					}
				}
			}

			//Build the list of servers hosting writable copies of each NC.
			foreach (DomainControllerReplicationInfo dcri in _dcs)
			{
				foreach (NamingContextInfo nci in dcri.HostedNcs)
				{
					if ((nci.IsWritable) && (!nci.NCName.Contains("schema")))
					{
						if (string.Compare(writableReferencesDC[nci.NCName].DCName, dcri.DCName) != 0)
						{
							if (!writableHostsInNC.ContainsKey(nci.NCName))
							{
								writableHostsInNC.Add(nci.NCName, new List<DomainControllerReplicationInfo>());
							}
							if (!writableHostsInNC[nci.NCName].Contains(dcri))
							{
								writableHostsInNC[nci.NCName].Add(dcri);
							}
						}
					}
				}
			}
			#endregion

			//Create header line for data returned
			DateTime startTime = DateTime.Now;
			WorkItemQueue wiq = new WorkItemQueue(_numThreads);
			Console.WriteLine();
			Console.WriteLine("Number Complete,Status,Server Name,Naming Context,Reference DC,Duration,Error Code,Error Message");
			#region Process writable ncs to create a single clean reference NC
			//Run one thread per writable partition.  I'm not sure if there will be concurrency issues if we run 
			// multiple instances of /removelingeringobjects against one box.
			foreach (string nc in writableHostsInNC.Keys)
			{
				if (!_bypassCleaningWritableNCs.Contains(nc))
				{
					object[] paramsToPass = { nc, writableHostsInNC[nc], writableReferencesDC[nc] };
					wiq.EnqueueWorkItem(cleanWritableNC, paramsToPass);
				}
			}
			wiq.WaitAllWorkItems();
			wiq.Dispose();
			TimeSpan referenceNCDuration = DateTime.Now - startTime;
			Console.WriteLine("Reference NCs cleaned in {0}h:{1}m:{2}s.  Cleaning everything else against reference NCs.", referenceNCDuration.TotalHours.ToString("N0"), referenceNCDuration.Minutes.ToString(), referenceNCDuration.Seconds.ToString());
			#endregion

			#region Clean everything else against reference NCs
			wiq = new WorkItemQueue(_numThreads);
			foreach (string namingContext in writableReferencesDC.Keys)
			{
				foreach (DomainControllerReplicationInfo dcri in _dcs)
				{
					if ((dcri[namingContext] != null) && (string.Compare(writableReferencesDC[namingContext].DCName, dcri.DCName) != 0))
					{
						LingeringObjectCleaner loc = new LingeringObjectCleaner(dcri.DCName, namingContext, writableReferencesDC[namingContext].NtDsaGuid);
						wiq.EnqueueWorkItem(clean, loc);
					}
				}
			}
			wiq.WaitAllWorkItems();
			wiq.Dispose();
			#endregion
			TimeSpan allCleanDuration = DateTime.Now - startTime;
			Console.WriteLine("All NCs {0} in {1}h:{2}m:{3}s.", _useAdvisoryMode ? "cleaned" : "scanned", allCleanDuration.TotalHours.ToString("N0"), allCleanDuration.Minutes.ToString(), allCleanDuration.Seconds.ToString());
			if (_useAdvisoryMode) Console.WriteLine("\tCheck individual server event logs for objects identified as lingering.");
		}

		private static void cleanWritableNC(object stateInfo)
		{
			string namingContext = (string)((object[])stateInfo)[0];
			List<DomainControllerReplicationInfo> dcsHostingWritableNC = (List<DomainControllerReplicationInfo>)((object[])stateInfo)[1];
			DomainControllerReplicationInfo targetDC = (DomainControllerReplicationInfo)((object[])stateInfo)[2];

			foreach (DomainControllerReplicationInfo referenceDC in dcsHostingWritableNC)
			{
				LingeringObjectCleaner loc = new LingeringObjectCleaner(targetDC.DCName, namingContext, referenceDC.NtDsaGuid);
				clean(loc);
			}
		}

		private static void clean(object stateInfo)
		{
			LingeringObjectCleaner loc = (LingeringObjectCleaner)stateInfo;

			if (!_outputRemoveLingeringObjectsCommandLine)
			{
				bool isCleaned = loc.Clean(_useAdvisoryMode);
				Console.WriteLine("{0},Success,{1},\"{2}\",{3},{4}h:{5}m:{6}s,{7},{8}", (Interlocked.Increment(ref _numComplete)).ToString(), loc.ServerName, loc.NamingContext, loc.ReferenceDC.ToString("B"), loc.Duration.TotalHours.ToString("N0"), loc.Duration.Minutes, loc.Duration.Seconds, loc.ErrorCode.ToString(), loc.ErrorMessage);
			}
			else
			{
				Console.WriteLine("repadmin /removelingeringobjects {0} {1} {2}" + (_useAdvisoryMode ? " /ADVISORY_MODE" : string.Empty), loc.ServerName, loc.ReferenceDC.ToString(), loc.NamingContext);
			}
		}
	}

	class LingeringObjectCleaner : IDisposable
	{
		private string _serverName, _namingContext, _errorMessage;
		private Guid _referenceDC;
		private NtDsApi.DsHandle _hDS;
		private bool _useAdvisoryMode;
		private uint _errorCode;
		private TimeSpan _duration;

		public string ServerName
		{
			get { return _serverName; }
		}

		public string NamingContext
		{
			get { return _namingContext; }
		}

		public Guid ReferenceDC
		{
			get { return _referenceDC; }
		}

		public string ErrorMessage
		{
			get { return _errorMessage; }
		}

		public uint ErrorCode
		{
			get { return _errorCode; }
		}

		public bool UseAdvisoryMode
		{
			get { return _useAdvisoryMode; }
			set { _useAdvisoryMode = value; }
		}

		public TimeSpan Duration
		{
			get { return _duration; }
		}

		public LingeringObjectCleaner(string serverName, string namingContext, Guid referenceDC)
		{
			_serverName = serverName;
			_namingContext = namingContext;
			_referenceDC = referenceDC;
		}

		public bool Clean()
		{
			return Clean(_useAdvisoryMode);
		}

		public bool Clean(bool useAdvisoryMode)
		{
			_errorCode = NtDsApi.DsBind(_serverName, null, out _hDS);

			if (_errorCode == 0)
			{
				DateTime startTime = DateTime.Now;
				_errorCode = NtDsApi.DsReplicaVerifyObjects(_hDS, _namingContext, _referenceDC, useAdvisoryMode ? NtDsApi.DS_EXIST_ADVISORY_MODE : 0);
				_errorMessage = "Error Verifying.";
				_duration = DateTime.Now - startTime;
				_hDS.Close();

				if (_errorCode == 0)
				{
                    _errorMessage = "Success";
					return true;
				}
			}
			else
			{
				_errorMessage = "Failed to bind.";
			}

			return false;
		}
		#region IDisposable Members

		public void Dispose()
		{
			if (_hDS != null)
			{
				//LingeringObjectCleaner.dsUnBind(ref _hDS);
			}
		}

		#endregion
	}
}
